# -*- coding: utf-8 -*-
# coding:utf-8

import time
import json
import hashlib
import Levenshtein
import wejudge
import wejudge.kernel.general as kernel
import wejudge.apps.problem.models as ProblemMdl
import wejudge.apps.asgn.models as AsgnModel
import wejudge.apps.contest.models as ContestModel


__author__ = 'lancelrq'


class JudgeServiceAPI(kernel.ViewerFramework):
    """评测机接口模块"""

    def __init__(self, request):
        kernel.ViewerFramework.__init__(self, request)
        self._navbar_action = 'judge_status'

    def receive_judge_result(self, sid):
        """
        接收评测结果（基础：转移result文件、核对信息等）
        :param sid: JudgeStatus ID
        :return:
        """
        self._action = kernel.const.VIEW_ACTION_JSON
        if not self._license_check():
            return
        result = self._request.POST.get("result")
        root = json.loads(result)
        status = ProblemMdl.JudgeStatus.objects.filter(id=sid)
        if status.exists():
            status = status[0]
            status.result = result
            status.flag = root.get('exitcode', 8)
            status.exe_mem = root.get('memused', 0)
            status.exe_time = root.get('timeused', 0)
            status.save()
            self.status_callback_proc(status)
            self._exchange_progrem_run_outdata(status, root.get('session_id'), root.get("outdatas"))
            self._arrange_judge_result(status)
            self._result = kernel.RESTStruct(True, data=True)

        else:
            self._result = kernel.RESTStruct(False, msg="Error: No status found.")

    def tdmaker_receive_judge_result(self, id):
        """
        [TDMaker]接收评测结果（写入全新的测试数据）
        :param sid: JudgeStatus ID
        :return:
        """
        self._action = kernel.const.VIEW_ACTION_JSON
        if not self._license_check():
            return
        result = self._request.POST.get("result")
        root = json.loads(result)
        tdq = ProblemMdl.TdmakerQueue.objects.filter(id=id)
        if tdq.exists():
            tdq = tdq[0]
            tdq.flag = root.get('exitcode', 8)
            tdq.memused = root.get('memused', 0)
            tdq.timeused = root.get('timeused', 0)
            tdq.save()
            session_id = root.get('session_id', '0')
            out_tmp_dir = kernel.LocalStorage(kernel.const.PROGRAM_RUN_OUTDATAS_TEMP, session_id)
            out_target_dir = kernel.LocalStorage(kernel.const.PROBLEM_TESTDATA_DIR, str(tdq.problem.id))
            testdata = tdq.problem.test_data.all()
            for td in testdata:
                handle = td.handle
                out_target_dir.clone_file("%s.out" % handle, out_tmp_dir.get_file_path("%s.outdata" % handle))
            out_tmp_dir.delete('')     # 删除当前目录
            self._result = kernel.RESTStruct(True, data=True)
        else:
            self._result = kernel.RESTStruct(False, msg="Error: No tdmaker status found.")

    def tdmaker_get_problem_judge_options(self, id):
        """
        [TDMaker]获取题目评测信息（包括测试数据集合）
        :param sid: Status的ID
        :return:
        """
        self._action = kernel.const.VIEW_ACTION_JSON
        if not self._license_check():
            return
        tdq = ProblemMdl.TdmakerQueue.objects.filter(id=id)
        if not tdq.exists():
            self._result = kernel.RESTStruct(False, msg="Error: No TDmaker status found.")
            return
        tdq = tdq[0]
        problem = tdq.problem
        demo_code = {}
        if problem.demo_code is not None and problem.demo_code.strip() != "":
            demo_code = json.loads(problem.demo_code)
        lang = demo_code.get('lang', 'gcc')

        content = demo_code.get("content", "")
        tds = []
        testdatas = problem.test_data.order_by("order")
        for td in testdatas:
            tds.append(td.dump_for_judge())
        if str(lang) == 'java':
            code_path = "Main.java"
            time_limit = problem.java_time_limit
            mem_limit = problem.java_memory_limit
        else:
            code_path = "m.%s" % kernel.const.SOURCE_CODE_EXTENSION.get(lang, "c")
            time_limit = problem.c_time_limit
            mem_limit = problem.c_memory_limit

        self._result = kernel.RESTStruct(True, data={
            'lang': lang,
            'problem_id': problem.id,
            'time_limit': time_limit,
            'memory_limit': mem_limit,
            'test_data': tds,
            'code_path': code_path,
            'demo_code': content
        })

    def get_problem_judge_options(self, sid):
        """
        获取题目评测信息（包括测试数据集合）
        :param sid: Status的ID
        :return:
        """
        self._action = kernel.const.VIEW_ACTION_JSON
        if not self._license_check():
            return
        status = ProblemMdl.JudgeStatus.objects.filter(id=sid)
        if not status.exists():
            self._result = kernel.RESTStruct(False, msg="Error: No status found.")
            return
        status = status[0]
        lang = status.lang
        problem = status.problem
        tds = []
        testdatas = problem.test_data.order_by("order")
        for td in testdatas:
            tds.append(td.dump_for_judge())
        if str(lang) == 'java':
            time_limit = problem.java_time_limit
            mem_limit = problem.java_memory_limit
        else:
            time_limit = problem.c_time_limit
            mem_limit = problem.c_memory_limit

        try:
            callback = json.loads(status.callback)
        except:
            callback = None

        ignore_pe = False
        if callback is not None and callback.get('provider', '') == 'asgn':
            asgn_id = callback.get('id')
            asgn = AsgnModel.Asgn.objects.filter(id=asgn_id)
            if asgn.exists():
                asgn = asgn[0]
                asgn_problem = asgn.problemset.filter(problem=status.problem)
                if asgn_problem.exists():
                    asgn_problem = asgn_problem[0]
                    ignore_pe = asgn_problem.ignore_pe

        self._result = kernel.RESTStruct(True, data={
            'lang': status.lang,
            'problem_id': problem.id,
            'status_id': status.id,
            'time_limit': time_limit,
            'memory_limit': mem_limit,
            'test_data': tds,
            'code_path': status.code_path,
            'ignore_pe': ignore_pe
        })

    def _exchange_progrem_run_outdata(self, status, session_id, outdatas):
        """
        将程序输出结果复制到永久存储文件夹
        :param status:
        :param outdatas:
        :return:
        """
        out_tmp_dir = kernel.LocalStorage(kernel.const.PROGRAM_RUN_OUTDATAS_TEMP, session_id)
        out_target_dir = kernel.LocalStorage(kernel.const.PROGRAM_RUN_OUTDATAS, str(status.id))
        if outdatas is not None:
            for key, val in outdatas.iteritems():
                out_target_dir.clone_file(val, out_tmp_dir.get_file_path(val))
        out_tmp_dir.delete('')     # 删除当前目录

    def _arrange_judge_result(self, status):
        """
        处理评测结果（处理用户题目访问记录，全局统计，Asgn模块分记等事项）
        :param status: 评测状态记录
        :return:status
        """
        # 生成用户个人的评测统计
        tmp = ProblemMdl.JudgeStatus.objects.filter(problem=status.problem, author=status.author)
        total = tmp.count()
        ac = tmp.filter(flag=0).count()
        # 写入
        pv = ProblemMdl.ProblemVisited.objects.filter(problem=status.problem, author=status.author)
        if pv.exists():
            pv = pv[0]
        else:
            pv = ProblemMdl.ProblemVisited()
            pv.problem = status.problem
            pv.author = status.author
        pv.submissions = total
        pv.accepted = ac
        pv.save()
        # 个人信息统计
        author = status.author
        pv_set = author.problemvisited_set
        count = ProblemMdl.JudgeStatus.objects.filter(author=author).count()
        if int(author.submissions) != int(count):
            author.submissions = int(count)
            author.accepted = ProblemMdl.JudgeStatus.objects.filter(author=author, flag=0).count()
            author.visited = pv_set.count()
            author.solved = pv_set.filter(accepted__gt=0).count()
            author.point_solved = author.solved * 2
            author.save()
        # 全局统计
        tmp = ProblemMdl.JudgeStatus.objects.filter(problem=status.problem)
        status.problem.total = tmp.count()
        status.problem.ac = tmp.filter(flag=0).count()
        status.problem.save()
        return status.problem.total, status.problem.ac

    def status_callback_proc(self, status):
        """
        评测结果额外内容处理程序
        :param status:
        :return:
        """
        try:
            callback = json.loads(status.callback)
        except:
            return

        if callback.get('provider', '') == 'asgn':
            self.__asgn_judge_callback(status, callback.get('id'))

        elif callback.get('provider', '') == 'contest':
            self.__contest_judge_callback(status, callback.get('id'))
            pass

    def __asgn_judge_callback(self, status, asgn_id):
        """
        评测控制权移交回调(Asgn)
        :param status:
        :param asgn_id:
        :return:
        """
        asgn = AsgnModel.Asgn.objects.filter(id=asgn_id)
        if asgn.exists():
            # 缓存测试数据数据定义
            td_score_precent = {}
            test_datas = ProblemMdl.TestData.objects.filter(problem=status.problem)
            for td in test_datas:
                td_score_precent[td.handle] = td.score_precent

            asgn = asgn[0]
            # 获取题目设置信息
            asgn_problem = asgn.problemset.filter(problem=status.problem)
            if not asgn_problem.exists():
                return
            asgn_problem = asgn_problem[0]
            ignore_pe = asgn_problem.ignore_pe
            pe_score = asgn_problem.pe_score
            asgn_problem.accepted = asgn.judge_status.filter(flag=0, problem__id=status.problem.id).count()
            asgn_problem.submission = asgn.judge_status.filter(problem__id=status.problem.id).count()
            asgn_problem.save()
            # 获取当前作者对单个题目的解决情况
            score = 0
            sol = AsgnModel.Solution.objects.filter(asgn=asgn, problems=status.problem, author=status.author)
            if sol.exists():
                sol = sol[0]
                sol_status = sol.judge_status.order_by('timestamp')
                sub, ac, pe = 0, 0, 0
                for st in sol_status:
                    sub += 1
                    if st.flag == 1:
                        pe += 1
                    if st.flag == 0:
                        ac += 1
                        score = 100
                        sol.first_ac_time = st.timestamp
                        break
                    else:
                        tscore = 0
                        try:
                            result = json.loads(st.result)
                            result = result.get("result", None)
                            if result is not None:
                                for key, val in result.iteritems():
                                    flag = val.get("result", -1)
                                    if flag == 0:
                                        tscore += td_score_precent.get(key, 0)
                                    elif ignore_pe and flag == 1:
                                        tscore += td_score_precent.get(key, 0) * (pe_score / 100.0)
                        except:
                            tscore = 0
                        # 取最大的一次
                        score = max(score, tscore)

                sol.score = score
                sol.submission, sol.accepted, sol.pe = sub, ac, pe
                sol.save()

            # ===== Report Counter ======
            report = AsgnModel.StuReport.objects.filter(asgn=asgn, student=status.author)
            if report.exists():
                self.__asgn_report_count_by_solutions(asgn, report[0])

    def __contest_judge_callback(self, status, contest_id):
        """
        评测控制权移交回调(Contest)
        :param status:
        :param contest_id:
        :return:
        """
        contest = ContestModel.Contest.objects.filter(id=contest_id)
        if contest.exists():
            contest = contest[0]
            sol = ContestModel.ContestSolution.objects.filter(
                contest=contest, problems=status.problem,
                author=status.author
            )
            if sol.exists():
                sol = sol[0]
                sol.first_ac_time = 0
                sol_status = sol.judge_status.order_by('timestamp')
                sub, ac = 0, 0
                for st in sol_status:
                    sub += 1
                    if st.flag == 0:
                        ac += 1
                        sol.first_ac_time = st.timestamp
                        break
                sol.submission = sub
                sol.accepted = ac
                sol.save()
            cproblem = contest.problemset.filter(problem=status.problem)
            if cproblem.exists():
                cproblem = cproblem[0]
                cproblem.accepted = contest.judge_status.filter(flag=0, problem__id=status.problem.id).count()
                cproblem.submission = contest.judge_status.filter(problem__id=status.problem.id).count()
                cproblem.save()

        # 评测通过，并且开启查重
        if status.flag == 0 and contest.cross_check:
            result = JudgeServiceAPI.__contest_cross_check_worker.delay(contest_id, status.id)

    @staticmethod
    @wejudge.celery_app.task()
    def __contest_cross_check_worker(contest_id, status_id):
        """
        查重celery工作器
        :return:
        """
        contest = ContestModel.Contest.objects.filter(id=contest_id)
        if not contest.exists():
            return False
        contest = contest[0]
        status = ProblemMdl.JudgeStatus.objects.filter(id=status_id)
        if not status.exists():
            return False
        status1 = status[0]
        problem_id = status1.problem.id
        cproblem = contest.problemset.filter(problem=status1.problem)
        if not cproblem.exists():
            return False
        cproblem = cproblem[0]

        # 检查是否为忽略查重项目
        if contest.cross_check_ignore_problem.filter(id=problem_id).count() > 0:
            return False

        pjs = contest.judge_status.all()                # 获取评测记录列表

        if pjs.count() > 0:
            for status2 in pjs:
                if status1.author.id == status2.author.id:  # 自己的评测记录可以直接无视
                    continue
                c1 = ContestModel.ContestCodeAnalysis.objects.filter(contest=contest, status1__author=status1.author, status2__author=status2.author).count()
                c2 = ContestModel.ContestCodeAnalysis.objects.filter(contest=contest, status1__author=status2.author, status2__author=status1.author).count()
                if c1 > 0 or c2 > 0:
                    exist = True
                else:
                    exist = False
                if not exist:  # 如果没有处理过
                    # 读取status1对应的代码
                    storage = kernel.LocalStorage(kernel.const.USER_UPLOADCODE_DIR, "")
                    fs = storage.open_file(status1.code_path)
                    code1 = fs.read().replace('\n', '').replace('\r', '').replace('\t', '').replace('\n', '')
                    fs.close()
                    # 读取status2对应的代码
                    fs = storage.open_file(status2.code_path)
                    code2 = fs.read().replace('\n', '').replace('\r', '').replace('\t', '').replace('\n', '')
                    fs.close()
                    # 读取记录
                    ratio = Levenshtein.ratio(code1, code2)
                    if ratio < contest.cross_check_ratio:
                        continue
                    cca = ContestModel.ContestCodeAnalysis()
                    cca.contest = contest
                    cca.cproblem = cproblem
                    cca.status1 = status1
                    cca.status2 = status2
                    cca.levenshtein_similarity_ratio = ratio
                    cca.save()
                    print u"查重录入：%s <-> %s: %.3f" % (status1.id, status2.id, ratio)
            return True

    def get_judge_status(self, sid):
        """
        前端：动态获取评测状态
        :param sid:
        :return:
        """
        self._action = kernel.const.VIEW_ACTION_JSON

        status = ProblemMdl.JudgeStatus.objects.filter(id=sid)
        if not status.exists():
            self._result = kernel.RESTStruct(False, msg="未找到评测状态")
            return
        status = status[0]
        result_detail = []
        ce_info = ""
        try:
            if status.result is not None and status.result != '':
                result = json.loads(status.result)
            else:
                result = {}
        except:
            result = {}

        if status.flag in [0, 1, 2, 3, 4, 5, 6]:
            detail = result.get('result', {})
            testdatas = status.problem.test_data.order_by('order')
            index = 1
            for td in testdatas:
                v = detail.get(td.handle, {})
                vflag = v.get('result', -4)
                vmem = v.get('memoryused', 0)
                vtime = v.get('timeused', 0)
                if vflag not in [0, 1, 2, 3, 4, 5, 6]:
                    vmem = "---"
                    vtime = "---"

                if vflag == 2:
                    vtime = '---'
                if vflag == 3:
                    vmem = '---'

                d = {
                    "memoryused": vmem,
                    "timeused": vtime,
                    "result": kernel.const.JUDGE_STATUS_FLAG_DESC.get(vflag, {"title": "未评测", "color": "danger"}),
                    "handle": td.handle,
                    "name": "测试数据%d" % index,
                }
                index += 1
                result_detail.append(d)

        if status.flag == 7:
            ce_info = result.get('ce_info', "")

        self._result = kernel.RESTStruct(True, data={
            "flag": status.flag,                                            # 状态
            "distance": int(time.time()) - status.timestamp,                # 等待时间
            "result_detail": result_detail,
            "ce_info": ce_info
        })

    def __asgn_report_count_by_solutions(self, asgn, report):
        """
        实验报告自动统计程序
        :param asgn:
        :param report:
        :return:
        """
        full_score = asgn.full_score
        asgn_problems = asgn.problemset.all()
        score = 0
        ac_cnt = 0
        total_cnt = 0
        solved_cnt = 0
        for ap in asgn_problems:
            sol = asgn.solution_set.filter(problems=ap.problem, author=report.student)
            sol = sol[0] if sol.exists() else None
            ac = True if (sol is not None) and (sol.accepted > 0) else False
            sol_score = sol.score if (sol is not None) else 0
            score += ap.score if ac else int(ap.score * (sol_score / 100.0))
            ac_cnt += sol.accepted if (sol is not None) else 0
            total_cnt += sol.submission if (sol is not None) else 0
            solved_cnt += 1 if ac else 0
        if score > full_score:
            score = full_score
        report.judge_score = score
        report.ac_counter = ac_cnt
        report.submission_counter = total_cnt
        report.solved_counter = solved_cnt
        report.modify_time = int(time.time())
        report.save()

    def __api_license_check(self, timestamp, randstr, signature):
        """
        接口访问权限检查
        :param timestamp:   请求时间戳
        :param randstr:     随机字符串
        :param signature:   校验码
        和微信类似，首先用secert，时间戳文本和随机字符串组成一个数组，然后按字典序排序，按顺序拼接成一个字符串，进行sha256加密，
        如果加密结果和signature相同，则视为校验成功
        :return:
        """
        secert = [
            kernel.const.JUDGE_SERVICE_API_SECERT,
            str(timestamp),
            str(randstr)
        ]
        secert.sort()       # 字典序
        secert = hashlib.sha256("".join(secert)).hexdigest()
        if str(secert) == str(signature):
            return True
        return False

    def _license_check(self):
        """
        权限检查
        :return:
        """
        check_flag = self.__api_license_check(
            self._request.GET.get('timestamp', ''), self._request.GET.get('randstr', ''), self._request.GET.get('signature', '')
        )
        if check_flag is False:
            self._result = kernel.RESTStruct(False, msg="License ERROR.")
            return False
        return True